package cn.edu.cug.cs.gtl.oi.utils.editors;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.FilteredImageSource;
import java.awt.image.MemoryImageSource;
import java.awt.image.RGBImageFilter;
import java.util.ArrayList;
import java.util.List;

/**
 * A color wheel lets you interactively select colors.
 *
 */
class ColorWheel extends Canvas
{
  private boolean WYSIWYGmode;
  private float[] hsvColor = new float[3];
  private int radius;

  // this image stores the basic color wheel image (with the brightness == 1)
  // to obtain an image with a different brightness (or value), use the filter
  // below with this image.
  private Image wheelImage = null;

  private List<Listener> wheelListeners;
  private boolean interactive;

  private static final int SPACE = 4;

  private WheelRGBImageFilter filter;
  private int Xmarker, Ymarker;
  private Dimension lastSize = new Dimension(0, 0);

  private boolean markerIsVisible;

  ColorWheel()
  {
    ListenMouse l = new ListenMouse();
    addMouseListener(l);
    addMouseMotionListener(l);

    WYSIWYGmode = false;
    markerIsVisible = false;
    hsvColor[0] = hsvColor[1] = 0;
    hsvColor[2] = 1;

    wheelListeners = new ArrayList<Listener>();
    interactive = false;

    Xmarker = getSize().width / 2;
    Ymarker = getSize().height / 2;

    // construct the filter with the beginning intensity 1
    filter = new WheelRGBImageFilter(1);
  }

  @Override
  public Dimension getPreferredSize()
  {
    return new Dimension(100, 100);
  }

  /**
   * Adds the specified listener to receive wheel's events.<br>
   * If listener l is null, no exception is thrown and no action is performed.
   *
   * @param listener
   *          the wheel listener
   */
  void addListener(Listener listener)
  {
    if ( listener != null )
      wheelListeners.add(listener);
  }

  /**
   * Removes the specified listener if present so that it no longer receives
   * wheel's events.
   *
   * @param listener
   *          the wheel listener
   * @return true if specified listener has been removed.
   */
  boolean removeListener(Listener listener)
  {
    return wheelListeners.remove(listener);
  }

  private void invokeValueChanged(float[] hsvColor)
  {
    for ( Listener listener : wheelListeners )
      listener.valueChanged(hsvColor);
  }

  private void invokeStartEditing(float[] hsvColor)
  {
    for ( Listener listener : wheelListeners )
      listener.startEditing(hsvColor);
  }

  private void invokeFinishEditing(float[] hsvColor)
  {
    for ( Listener listener : wheelListeners )
      listener.finishEditing(hsvColor);
  }

  boolean isInteractive()
  {
    return interactive;
  }

  /**
   * Routine to tell the color wheel what the current HSV color is. <BR>
   * <BR>
   * NOTE: if calling setBaseColor() changes the marker position the
   * valueChanged callbacks will be called with the new hsv color.
   *
   * @param hsv
   *          is a 3 float array representing an HSV color.
   */
  void setBaseColor(float[] hsv)
  {
    boolean valueChanged = (hsvColor[2] != hsv[2]);
    boolean redrawColors = (WYSIWYGmode && valueChanged);
    boolean redrawMarker = ((hsvColor[0] != hsv[0]) || (hsvColor[1] != hsv[1]));

    // assign new color
    hsvColor[0] = hsv[0];
    hsvColor[1] = hsv[1];
    hsvColor[2] = hsv[2];

    if ( redrawColors )
    {
      Graphics g = getGraphics();
      if ( g != null )
      {
        // clear the marker
        g.setColor(Color.white);
        g.setXORMode(Color.black);
        g.drawRect(Xmarker - 3, Ymarker - 3, 6, 6);

        redrawWheel(hsvColor[2]);
        // redraw the marker
        g.setColor(Color.white);
        g.setXORMode(Color.black);
        g.drawRect(Xmarker - 3, Ymarker - 3, 6, 6);
      }
    }
    drawWheelMarker();

    if ( redrawMarker || valueChanged )
      invokeValueChanged(hsvColor);
  }

  float[] getBaseColor()
  {
    return hsvColor;
  }

  void setWYSIWYG(boolean wysiwyg)
  {
    if ( WYSIWYGmode == wysiwyg )
      return;

    WYSIWYGmode = wysiwyg;

    redrawWheel(hsvColor[2]);
  }

  boolean isWYSIWYG()
  {
    return WYSIWYGmode;
  }

  private void buildImage(Dimension d)
  {
    int min = (d.width < d.height) ? d.width : d.height;

    int width = min - 2 * SPACE; // keep a place for the 3D border

    int[] pixels = new int[width * width];

    int RADIUS = radius * radius;
    float PI2 = (float) (2 * Math.PI);
    for ( int y = -radius; y < radius; y++ )
    {
      int Y = y * y;
      int tmpW = (y + radius) * width;
      for ( int x = -radius; x < radius; x++ )
      {
        int X = x * x;
        int tmpX = x + radius;
        if ( (X + Y) <= RADIUS )
        {
          float s = (float) Math.sqrt(X + Y) / radius;
          if ( s > 1 )
            s = 1;
          float angle = (float) Math.atan2(-y, x);
          if ( angle < 0 )
            angle += PI2;

          float h = angle / PI2;
          pixels[tmpW + tmpX] = Color.HSBtoRGB(h, s, 1);
        }
        else
          pixels[tmpW + tmpX] = 0; // alpha == 0 then allows another background
                                   // color.
      }
    }

    wheelImage = createImage(new MemoryImageSource(width, width, pixels, 0, width));
  }

  @Override
  public void paint(Graphics g)
  {
    super.paint(g);
    Dimension d = getSize();
    // draw wheel surroundings
    g.draw3DRect(0, 0, d.width - 1, d.height - 1, false);

    if ( !d.equals(lastSize) )
    {
      lastSize = d;
      int min = (d.width < d.height) ? d.width : d.height;
      int width = min - 2 * SPACE; // keep a place for the 3D border
      radius = width / 2;

      float rad = hsvColor[1] * radius;
      float angle = 2 * (float) Math.PI * hsvColor[0];

      Xmarker = (int) (d.width / 2F + (rad * (float) Math.cos(angle)));
      Ymarker = (int) (d.height / 2F - (rad * (float) Math.sin(angle)));
      buildImage(d);
    }

    redrawWheel(hsvColor[2]);

    // redraw the marker
    markerIsVisible = false; // paint has refreshed the component
    drawWheelMarker(Xmarker, Ymarker);
  }

  private void redrawWheel(float intensity)
  {
    if ( wheelImage == null )
      return;
    Graphics g = getGraphics();
    if ( g == null )
      return;

    Dimension d = getSize();

    Image img;
    if ( WYSIWYGmode )
    {
      filter.setIntensity(intensity);
      img = createImage(new FilteredImageSource(wheelImage.getSource(), filter));
    }
    else
      img = wheelImage;

    g.drawImage(img, d.width / 2 - radius, d.height / 2 - radius, null);
  }

  private synchronized void clearMarker()
  {
    Graphics g = getGraphics();
    if ( markerIsVisible && (g != null) )
    {
      g.setColor(Color.white);
      g.setXORMode(Color.black);
      g.drawRect(Xmarker - 3, Ymarker - 3, 6, 6);
      markerIsVisible = false;
    }
  }

  private void drawWheelMarker()
  {
    // find radius and angle from hsv color, from which position is determined.
    float rad = hsvColor[1] * radius;
    float angle = 2 * (float) Math.PI * hsvColor[0];
    Dimension d = getSize();

    int x = (int) (d.width / 2 + (rad * (float) Math.cos(angle)));
    int y = (int) (d.height / 2 - (rad * (float) Math.sin(angle)));
    drawWheelMarker(x, y);
  }

  private synchronized void drawWheelMarker(int x, int y)
  {
    Graphics g = getGraphics();
    if ( g == null )
      return;

    clearMarker();

    g.setColor(Color.white);
    g.setXORMode(Color.black);
    g.drawRect(x - 3, y - 3, 6, 6);
    markerIsVisible = true;
    Xmarker = x;
    Ymarker = y;
  }

  private void moveWheelMarker(int x, int y)
  {
    float xCircle, yCircle;
    Dimension d = getSize();
    xCircle = x - d.width / 2;
    yCircle = d.height / 2 - y;

    // find the saturation based on distance
    float s = (float) Math.sqrt(xCircle * xCircle + yCircle * yCircle) / radius;
    if ( s > 1 )
      s = 1;

    // now find the hue based on the angle
    float angle = (float) Math.atan2(yCircle, xCircle);
    if ( angle < 0 )
      angle += (float) (2 * Math.PI);
    float h = angle / (float) (2 * Math.PI);

    // check if redraw and callback are needed
    if ( (hsvColor[0] != h) || hsvColor[1] != s )
    {
      hsvColor[0] = h;
      hsvColor[1] = s;
      drawWheelMarker(x, y);
      invokeValueChanged(hsvColor);
    }
  }

  /**
   * Defines an object which listens for wheel's events.<br>
   * This object will be notified when color edition starts, when color value
   * has changed and when edition is finished.
   *
   */
  static class Listener
  {
    /**
     * Invoked when color edition starts.
     *
     * @param hsvColor
     *          current color
     */
    public void startEditing(float[] hsvColor)
    {};

    /**
     * Invoked when color edition is finished.
     *
     * @param hsvColor
     *          current color
     */
    public void finishEditing(float[] hsvColor)
    {};

    /**
     * Invoked when color value has changed.
     *
     * @param hsvColor
     *          new color value
     */
    public void valueChanged(float[] hsvColor)
    {};
  }

  class ListenMouse extends MouseAdapter implements MouseMotionListener
  {
    @Override
    public void mousePressed(MouseEvent e)
    {
      int x, y;

      Dimension d = getSize();
      // check if click is in color wheel
      x = e.getX() - d.width / 2;
      y = e.getY() - d.height / 2;
      if ( (x * x + y * y) <= ((radius + 1) * (radius + 1)) )
      {
        invokeStartEditing(hsvColor);
        interactive = true;
        moveWheelMarker(e.getX(), e.getY());
      }
    }

    @Override
    public void mouseReleased(MouseEvent e)
    {
      if ( interactive )
      {
        interactive = false;
        invokeFinishEditing(hsvColor);
      }
    }

    @Override
    public void mouseDragged(MouseEvent e)
    {
      int x, y;
      Dimension d = getSize();
      x = e.getX() - d.width / 2;
      y = e.getY() - d.height / 2;
      if ( (x * x + y * y) > ((radius + 1) * (radius + 1)) )
      {
        if ( x == 0 )
          y = radius;
        else
        {
          double angle = Math.atan(y / (float) x);
          if ( x < 0 )
            angle += Math.PI;
          x = (int) (Math.cos(angle) * radius);
          y = (int) (Math.sin(angle) * radius);
        }
      }
      x += d.width / 2;
      y += d.height / 2;
      moveWheelMarker(x, y);
    }

    @Override
    public void mouseMoved(MouseEvent e)
    {}
  }

  /**
   * Be aware that this filter is only valid using an image with colors (if they
   * were in HSV format) with brightness equals to 1. <BR>
   * <BR>
   * This filter returns an image with a new brightness (intensity).
   */
  class WheelRGBImageFilter extends RGBImageFilter
  {
    float intensity;

    public WheelRGBImageFilter(float intensity)
    {
      if ( (intensity >= 0) && (intensity <= 1) )
        this.intensity = intensity;
      else
        this.intensity = 1;

      canFilterIndexColorModel = true;
    }

    @Override
    public int filterRGB(int x, int y, int rgb)
    {
      int red = ((int) (((rgb & 0x00FF0000) >> 16) * intensity)) << 16;
      int green = ((int) (((rgb & 0x0000FF00) >> 8) * intensity)) << 8;
      int blue = (int) ((rgb & 0x000000FF) * intensity);
      return (rgb & 0xFF000000) | red | green | blue;
    }

    public void setIntensity(float intensity)
    {
      if ( (intensity >= 0) && (intensity <= 1) )
        this.intensity = intensity;
    }
  }
}
